#ifndef __MATRIX__HPP
#define __MATRIX__HPP

//#define DEBUG__

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include "util.hpp"

#include <boost/shared_ptr.hpp>

using namespace std;
using namespace boost;

// declare matrix class for subsequent use
class Matrix;

class Iter
{
public:
	Iter(float f) : f_(f) {};
	Iter(const Iter& i) : f_(i.f_) {};
	Iter operator=(const Iter& i) {f_ = i.f_;};
	Iter operator++() {++f_; return *this;};
	Iter operator+=(const Iter& i) {f_ += i.f_; return *this;};
	bool operator==(const Iter& i) {return f_ == i.f_;}
	bool operator!=(const Iter& i) {return f_ != i.f_;}
	bool operator<(const Iter& i) {return f_ < i.f_;}
	bool operator>(const Iter& i) {return f_ > i.f_;}
	bool operator<=(const Iter& i) {return f_ <= i.f_;}
	bool operator>=(const Iter& i) {return f_ >= i.f_;}
	bool operator&&(const Iter& i) {return f_ && i.f_;}
	bool operator||(const Iter& i) {return f_ || i.f_;}
	Iter add(const Iter& i) {Iter ret(f_ + i.f_); return ret;};
	Iter sub(const Iter& i) {Iter ret(f_ - i.f_); return ret;};
	Iter mul(const Iter& i) {Iter ret(f_ * i.f_); return ret;};
	Iter div(const Iter& i) {Iter ret(f_ / i.f_); return ret;};
//	Iter mod(const Iter& i) {Iter ret(f_ % i.f_); return ret;};
//	Matrix add(const Matrix& m) {return m.add(Matrix(f_));};
//	Matrix sub(const Matrix& m) {return m.sub(Matrix(f_));};
//	Matrix mul(const Matrix& m) {return m.mul(Matrix(f_));};
//	Matrix div(const Matrix& m) {return m.div(Matrix(f_));};
//	Matrix div(const Matrix& m) {return m.mod(Matrix(f_));};
//	Matrix operator==(const Matrix& m) {return f_ == m.getFloatValue();}
//	Matrix operator!=(const Matrix& m) {return f_ != m.getFloatValue();}
//	Matrix operator<(const Matrix& m) {return f_ < m.getFloatValue();}
//	Matrix operator>(const Matrix& m) {return f_ > m.getFloatValue();}
//	Matrix operator<=(const Matrix& m) {return f_ <= m.getFloatValue();}
//	Matrix operator>=(const Matrix& m) {return f_ >= m.getFloatValue();}
//	Matrix operator&&(const Matrix& m) {return f_ && m.getFloatValue();}
//	Matrix operator||(const Matrix& m) {return f_ || m.getFloatValue();}
	float getFloatValue() {return f_;};
	float f_;
};
class RangeList
{
public:
	RangeList() : _rs(0) {};
	RangeList(float r1) : _rs(1), _r1(r1) {};
	RangeList(float r1, float r2): _rs(2),  _r1(r1), _r2(r2) {};
	float _r1, _r2; 
	int _rs;
};

class TextureID
{
public:
	TextureID() 
	{
		glGenTextures (1, &id_);
	};
	~TextureID()
	{
		glDeleteTextures (1, &id_);
	}
	GLuint id_;
};
typedef shared_ptr<TextureID> TextureIDPtr;


class Matrix
{
public:
	typedef shared_ptr<Matrix> MatrixPtr;
	Matrix(const Matrix& m) : w_(m.w_), h_(m.h_), texID_(m.texID_) 
	{
	}
	Matrix(const Matrix &h, const Matrix &w) : texID_(new TextureID())  
	{
		w_ = w.getFloatValue();
		h_ = h.getFloatValue();
		setupTexture();
		assign(0.0);
	};
	Matrix(int h, int w, int , int) : w_(w), h_(h), texID_(new TextureID())  
	{
		setupTexture();
	};
	Matrix(int h, int w) : w_(w), h_(h), texID_(new TextureID())  
	{
		setupTexture();
		assign(0.0);
	};
	Matrix(float f) : w_(1), h_(1), texID_(new TextureID()) 
	{
		setupTexture();		
		assign(f);
	};
	Matrix(float f, int h, int w) : w_(w), h_(h), texID_(new TextureID()) 
	{
		setupTexture();		
		assign(f);
	};
	Matrix(float f, const Matrix &h, const Matrix &w) : texID_(new TextureID()) 
	{
		w_ = w.getFloatValue();
		h_ = h.getFloatValue();
		setupTexture();
		assign(f);
	};
	Matrix(const Iter& i) : w_(1), h_(1), texID_(new TextureID())
	{
		setupTexture();		
		assign(i.f_);		
	}

	Matrix& operator=(const Matrix &m) 
	{
		w_ = m.w_; h_ = m.h_; texID_ = m.texID_;		
	}
	string print() const
	{
		vector<float> data(w_*h_);
		getTexture(data);
		return printVector(data);
	};
	Matrix slice(RangeList range1, RangeList range2) 
	{
		int x, y, w, h;
		getRange(range1, range2, x, y, w, h);
		Matrix ret(h, w);
		compute(ret.texID_->id_, texID_->id_, 0, ret.w_, ret.h_, "slice", 0, x, y);
		return ret;
	};
	Matrix& assign(const vector<float>& data, int h, int w) 
	{
		w_ = w;
		h_ = h;
		setupTexture();
		setTexture(data);
	}
	Matrix& assign(float val) 
	{
		vector<float> data(w_*h_, val);
		setTexture(data);
	}
	Matrix& assign(const Matrix& b) 
	{
		w_ = b.w_;
		h_ = b.h_;
		texID_ = b.texID_;
		return *this;
	};
	void sliceAssign(const RangeList& range1, const RangeList& range2, float val) 
	{
		int x, y, w, h;
		getRange(range1, range2, x, y, w, h);
		//printf ("%d, %d, %d, %d\n", x, w, y, h);
		vector<float> data(w*h, val);
		setTextureSub(data, x, y, w, h);
	};
	void sliceAssign(const RangeList& range1, const RangeList& range2, const Matrix& b) 
	{
		if (usingFBO) {
			int x, y, w, h;
			getRange(range1, range2, x, y, w, h);
			bindTexToFBO(b.texID_->id_);
			glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
			glBindTexture(textureParameters.texTarget, texID_->id_);
			glCopyTexSubImage2D(textureParameters.texTarget, 0, x, y, 0, 0, w, h);
			checkGLErrors("copytexsubimage");
		} 
		else 
		{
//			int x, y, w, h;
//			getRange(range1, range2, x, y, w, h);
//			//printf ("%d, %d, %d, %d\n", x, w, y, h);
//			compute(texID_->id_, b.texID_->id_, 0, w, h, "slice", 0, 0, 0, x, y);
			if(b.w_==1 && b.h_==1)
			{
				sliceAssign(range1, range2, b.getFloatValue());
			}
			else
			{
				int x, y, w, h;
				getRange(range1, range2, x, y, w, h);
				//printf ("%d, %d, %d, %d\n", x, w, y, h);
				compute(texID_->id_, b.texID_->id_, 0, w, h, "slice", 0, 0, 0, x, y);
			}
		}
	};
	void getRange(const RangeList& range1, const RangeList& range2, int& x, int& y, int& w, int& h)
	{
		// rows
		if (range1._rs ==0)
		{
			y = 0;
			h = h_;
		}
		else if (range1._rs ==1)
		{
			if ( range1._r1 < 0 ) {
				cerr << "Error: No negative row indices. Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			if (range1._r1 >= h_) {
				cerr << "Error: Out-of-bounds row value: " << range1._r1 << ". Exiting program.\n";
				cleanupMatPix();
				exit(1);							
			}
			y = range1._r1;
			h = 1;
		}
		else if (range1._rs ==2)
		{
			if ( range1._r1 < 0 ) {
				cerr << "Error: No negative row indices. Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			if ( range1._r1 > range1._r2 ) {
				cerr << "Error: Invalid row range. Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			if ( range1._r2 >= h_ ) {
				cerr << "Error: Out-of-bounds row index: " << range1._r2 << ". Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			y = range1._r1;
			h = range1._r2-range1._r1+1;
		}
		// columns
		if (range2._rs ==0)
		{
			x = 0;
			w = w_;
		}
		else if (range2._rs ==1)
		{
			if ( range2._r1 < 0 ) {
				cerr << "Error: No negative column indices. Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			if (range2._r1 >= w_) {
				cerr << "Error: Out-of-bounds column value: " << range2._r1 << ". Exiting program.\n";
				cleanupMatPix();
				exit(1);							
			}
			x = range2._r1;
			w = 1;
		}
		else if (range2._rs ==2)
		{
			if ( range2._r1 < 0 ) {
				cerr << "Error: No negative column indices. Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			if ( range2._r1 > range2._r2 ) {
				cerr << "Error: Invalid column range. Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			if ( range2._r2 >= w_ ) {
				cerr << "Error: Out-of-bounds column index: " << range2._r2 << ". Exiting program.\n";
				cleanupMatPix();
				exit(1);			
			}
			x = range2._r1;
			w = range2._r2-range2._r1+1;
		}
//		cout << "x = " << x << "; y = " << y << endl;
//		cout << "w = " << w << "; h = " << h << endl;
	}
	Matrix transpose()
	{
		Matrix ret(w_, h_);
		compute(ret.texID_->id_, texID_->id_, 0, ret.w_, ret.h_, "trans");
		return ret;
	};

	Matrix unaryOp(const char* op)
	{
		Matrix ret(w_, h_);
		compute(ret.texID_->id_, texID_->id_, 0, w_, h_, op);
		assign(ret);
		return ret;
	};
	Matrix binOp(const Matrix& b, const char* op)
	{
		Matrix ret(h_, w_, 0, 0);
		Matrix b_new(b);
		// runtime semantic analysis
		if ( (w_ != b.w_) || (h_ != b.h_) ) {
			if ( b.h_==1 && b.w_==1 ) {
				b_new.assign(Matrix(b.getFloatValue(),h_,w_));
			}
			else if ( h_==1 && w_==1 ) {
				ret = Matrix(b.h_, b.w_, 0, 0);
				Matrix self_new(getFloatValue(),b.h_,b.w_);
				compute(ret.texID_->id_, self_new.texID_->id_, b_new.texID_->id_, ret.w_, ret.h_, op);
				return ret;
			}
			else {
				cerr << "Error: Matrix dimensions do not agree. Exiting program.\n";
				cerr << "Left Matrix size: (" << h_ << ", " << w_ << ")\n";
				cerr << "Right Matrix size: (" << b.h_ << ", " << b.w_ << ")\n";
				cleanupMatPix();
				exit(1);
			}
		}
//		cout << "Left Matrix size: (" << h_ << ", " << w_ << ")\n";
//		cout << "Right Matrix size: (" << b_new.h_ << ", " << b_new.w_ << ")\n";
		compute(ret.texID_->id_, texID_->id_, b_new.texID_->id_, w_, h_, op);
		return ret;
	};
	Matrix mul(const Matrix& b)
	{
		return binOp(b, "*");
	};
	Matrix add(const Matrix& b) 
	{
		return binOp(b, "+");
	};
	Matrix sub(const Matrix& b)
	{
		return binOp(b, "-");
	};
	Matrix div(const Matrix& b) 
	{
		return binOp(b, "/");
	};
	Matrix mod(const Matrix& b) 
	{
		return binOp(b, "%");
	};
	Matrix operator==(const Matrix &b)  {
		return binOp(b, "==");
	};
	Matrix operator!=(const Matrix &b)  {
		return binOp(b, "!=");
	};
	Matrix operator<(const Matrix &b)  {
		return binOp(b, "<");
	};
	Matrix operator>(const Matrix &b)  {
		return binOp(b, ">");
	};
	Matrix operator<=(const Matrix &b)  {
		return binOp(b, "<=");
	};
	Matrix operator>=(const Matrix &b)  {
		return binOp(b, ">=");
	};
	Matrix operator&&(const Matrix &b)  {
		return binOp(b, "&&");
	};
	Matrix operator||(const Matrix &b)  {
		return binOp(b, "||");
	};
	Matrix operator++(int x) {
		return unaryOp("++");
	};
	Matrix operator--(int x) {
		return unaryOp("--");
	};
	Matrix negate() {
		return unaryOp("negate");
	};
	Matrix notOp() {
		return unaryOp("!");
	}
	float getFloatValue() const {
		if(!(w_ == 1 && h_ == 1))
			printf("Converting to Float: Not 1x1 Matrix!\n");
		vector<float> data(1);
		getTexture(data);
		return data[0];
	}
	Matrix dot(const Matrix& b) 
	{
		if(!(w_ == b.h_))
		{
			if ( b.h_==1 && b.w_==1 ) {
				Matrix ret(h_, w_);
				Matrix b_new(b.getFloatValue(),h_,w_);
				compute(ret.texID_->id_, texID_->id_, b_new.texID_->id_, ret.w_, ret.h_, "*");
				return ret;
			}
			else if ( h_==1 && w_==1 ) {
				Matrix ret(b.h_, b.w_);
				Matrix self_new(getFloatValue(),b.h_,b.w_);
				compute(ret.texID_->id_, self_new.texID_->id_, b.texID_->id_, ret.w_, ret.h_, "*");
				return ret;
			}
			else {
				cerr << "Matrix Dimensions for Dot Product Don't agree\n";
				cerr << "Left Matrix size: (" << h_ << ", " << w_ << ")\n";
				cerr << "Right Matrix size: (" << b.h_ << ", " << b.w_ << ")\n";
				exit(0);
			}
		}
//		cout << "Left Matrix size: (" << h_ << ", " << w_ << ")\n";
//		cout << "Right Matrix size: (" << b.h_ << ", " << b.w_ << ")\n";
		Matrix ret(h_, b.w_);
		compute(ret.texID_->id_, texID_->id_, b.texID_->id_, ret.w_, ret.h_, "dot", w_);
//		cout << "Return Matrix size: (" << ret.h_ << ", " << ret.w_ << ")\n";
		return ret;
	};
	
	int w_, h_; // texSize_;
	TextureIDPtr texID_;

private:
	string printVector(vector<float> data) const
	{
		int ind = 0;
		ostringstream ss;
		for(int i = 0; i < h_; i++)
		{
			for(int j = 0; j < w_; j++) {
				ss << data[ind++];
				if (j < w_-1)
					 ss << ", ";
			}
			ss << "\n";
		}
		return ss.str();
	}
	void setupTexture() {
		glBindTexture(textureParameters.texTarget, texID_->id_);
		glTexParameteri(textureParameters.texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(textureParameters.texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(textureParameters.texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameteri(textureParameters.texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP);
		glTexImage2D(textureParameters.texTarget, 0,
				textureParameters.texInternalFormat, w_, h_, 0,
				textureParameters.texFormat, GL_FLOAT, 0);
		// check if that worked
		if (glGetError() != GL_NO_ERROR) {
			printf("glTexImage2D(): [FAIL]\n");
		}
	}
	void getTexture(vector<float>& data) const {
		if (usingFBO) {
//		if (false) {
			bindTexToFBO(texID_->id_);
			glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
			glReadPixels(0, 0, w_, h_, textureParameters.texFormat,
					GL_FLOAT, &data[0]);
		} else {
			glBindTexture(textureParameters.texTarget, texID_->id_);
			glGetTexImage(textureParameters.texTarget, 0,
					textureParameters.texFormat, GL_FLOAT, &data[0]);
		}
		checkGLErrors("transfer from texture");
	}
	void setTexture(const vector<float>& data) {
		glBindTexture(textureParameters.texTarget, texID_->id_);
		glTexSubImage2D(textureParameters.texTarget, 0, 0, 0, w_, h_,
				textureParameters.texFormat, GL_FLOAT, &data[0]);
		checkGLErrors("transfer to texture");
	}
	void setTextureSub(const vector<float>& data, int x, int y, int w, int h) {
		//cout << "(x,y,w,h)" << x << ", " << y << ", " << w << ", " << h << endl;
		glBindTexture(textureParameters.texTarget, texID_->id_);
		glTexSubImage2D(textureParameters.texTarget, 0, x, y, w, h,
				textureParameters.texFormat, GL_FLOAT, &data[0]);
		checkGLErrors("transfer to texture");
	}
};


ostream& operator << (ostream& os, const Matrix& m)
{
  return os << m.print();
}
ostream& operator << (ostream& os, const TextureID& t)
{
  return os << t.id_;
}
ostream& operator << (ostream& os, const Iter& i)
{
  return os << i.f_ << endl;
}

#endif //__MATRIX__HPP
